/**
   @file random.c
   @brief Functions that make up the generator's random number interface.
 */
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <pthread.h>
#include <gsl/gsl_rng.h>
#include <gsl/gsl_randist.h>
#include "random.h"

/** The uniform random number generator used by the workload generator.*/
gsl_rng *wg_rng;

/** A constant probability distribution. */
struct constant_distribution {
  double constant; /**< The distibution's constant value. x*/
};

/** An exponential probability distribution. */
struct exponential_distribution {
  double lambda; /**< The rate parameter of the distribution.*/
};

/** A normal probability distribution. */
struct normal_distribution {
  double mean; /**< Distribution's mean */
  double std; /**< Distribution's standard deviation */
};

/** A uniform probability distribution */
struct uniform_distribution {  
  double min; /**< A random variable's minimum value */
  double max; /**< A random variable's maximum value */
};

/** A list of probability distributions to be used inside of an array as a hash table. */
typedef struct distribution_list distribution_list;

/** A list of random number generators. */
typedef struct rng_list rng_list;

/** Gets a random number from  */
static double generate_random_number(char *, void *);

/** Produce a random number from a constant_distribution. */
static double rand_const(void *);

/** Produces a random variable from the standard normal distribution. */
static double rand_z();

/** Produce a random number from a normal_distriubtion. */ 
static double rand_norm(void *);

/** Produce a random number from an exponential_distribution. */
static double rand_exp(void *);

/** Produce a random number from a uniform_distribution. */
static double rand_uni(void *);

/** Get a constant_distribution from a JSON object. */
static void *build_constant_distribution(cJSON *);

/** Get an exponential_distribution from a JSON object. */
static void *build_exponential_distribution(cJSON *);

/** Get a uniform_distribution from a JSON object. */
static void *build_uniform_distribution(cJSON *);

/** Get a normal distribution from a JSON object. */
static void *build_normal_distribution(cJSON *);

/** Fetches a probability distribution by its string. */
static distribution_list *get_distribution(char *);

/** Adds support for a probability distribution to the workload generator. */
static void add_distribution(char *, void*(*)(cJSON *), double(*)(void *));

/** The hash table in which all supported probability distributions are stored. */
static distribution_list *distribution_table[HASHSIZE];

/** The hash table in which all random number generators are stored. */
static rng_list *rng_table[HASHSIZE];

/** Mutexes that allow thread-safe access to probability distributions. */
static pthread_mutex_t distributions_mutex[HASHSIZE];

/** Mutexes that allow thread-safe access to drand48_data buffers. */
static pthread_mutex_t rng_buffers_mutex[HASHSIZE];

/** Mutexes that allow thread-safe  access to random number generators. */
static pthread_mutex_t rng_mutex[HASHSIZE];

/** A list of probability distributions to be used inside of an array as a hash table. */
struct distribution_list {
  distribution_list *next;
  char *key; /**< String identifying the probability distribution. */
  void *(*setup)(cJSON *); /**< Function to setup a probability distribution from a JSON object. */
  double (*get_number)(void *); /**< Function to construct a random number from a given */
};

/** A list of random number generators to be stored inside a hash table. */
struct rng_list {
  rng_list *next;
  char *key; /**< String identifying the random number generator inside the workload generator. */
  char *type; /**< The type of random number, identifies a distribution inside distribution_table. */
  void *args; /**< A generic pointer to a probability distribution structure. The specific type of structure depends on the type of the random number generator. */
};

void setup_random_environment()
{
  //Setup the random number generator
  const gsl_rng_type * T;
  gsl_rng_env_setup();
  T = gsl_rng_default;
  wg_rng = gsl_rng_alloc(T);
  
  setup_mutex_array(distributions_mutex);
  setup_mutex_array(rng_mutex);
  add_distribution("constant", build_constant_distribution, rand_const);
  add_distribution("uniform", build_uniform_distribution, rand_uni);
  add_distribution("exponential", build_exponential_distribution, rand_exp);
  add_distribution("normal", build_normal_distribution, rand_norm);
}

/**
 * @param s The string to hash.
 */ 
hashed hash(char *s) {
  unsigned hashval;
  for (hashval = 0; *s != '\0'; s++)
    {
      hashval = *s + 31 * hashval;
    }
  return hashval % HASHSIZE;
}


hashed int_hash(unsigned long int x){
  return x % HASHSIZE;
}

void *build_constant_distribution(cJSON *json) {
  cJSON *c = cJSON_GetObjectItem(json, "value");
  if(!c || c->type != 3 ) {
    return NULL; /** Check for correct type. */
  }
  double constant_value = c->valuedouble;
  struct constant_distribution *d = malloc(sizeof(struct constant_distribution));
  d->constant = constant_value;
  return (void*) d;
}

void *build_exponential_distribution(cJSON *json) {
  cJSON *c = cJSON_GetObjectItem(json, "lambda");
  if(!c || c->type != 3) {
    return NULL; /** Check for correct type. */
  }
  struct exponential_distribution *d = malloc(sizeof(struct exponential_distribution));
  d->lambda = c->valuedouble;
  return (void *) d;
}

void *build_normal_distribution(cJSON *json) {
  cJSON *json_mean = cJSON_GetObjectItem(json, "mean");
  cJSON *json_std = cJSON_GetObjectItem(json, "std");
  if(!json_mean || !json_std || json_mean->type != 3 || json_std->type != 3){
    return NULL;
  }
  struct normal_distribution *d = malloc(sizeof(struct normal_distribution));
  d->mean = json_mean->valuedouble;
  d->std = json_std->valuedouble;
  return (void *) d;
}

void *build_uniform_distribution(cJSON *json) {
  cJSON *json_min = cJSON_GetObjectItem(json,"min");
  cJSON *json_max = cJSON_GetObjectItem(json,"max");
  if(!json_min || !json_max || json_min->type != 3 || json_max->type != 3) {
    return NULL;
  }
  struct uniform_distribution *d = malloc(sizeof(struct uniform_distribution));
  d->min = json_min->valuedouble;
  d->max = json_max->valuedouble;
  return (void *) d;
}

double get_random_number(char *key)
{
  if(!key) {
    return 0;
  }
  /* Check for calls to the default random number generator. Gives a number x in [0,1). */
  if(strcmp(key, "wg_rand") == 0) {
    return gsl_rng_uniform (wg_rng);
  }
  
  hashed target = hash(key);
  pthread_mutex_lock(&rng_mutex[target]);
  rng_list *p = rng_table[target];
  for(p; p != NULL; p = p->next)
    {
      if(strcmp(key, p->key) == 0)
	{
	  pthread_mutex_unlock(&rng_mutex[target]);
          return generate_random_number(p->type,p->args);
        }
    }
  pthread_mutex_unlock(&rng_mutex[target]);
  return 0;
}

int setup_rng(char *key, cJSON *json)
{
  hashed target = hash(key);
  cJSON *distribution_type_json = (cJSON_GetObjectItem(json, "type"));
  if(!distribution_type_json || distribution_type_json->type != 4) {
    perror("No distribution type given.\n"); exit(1);
  }
  char *distribution_type = distribution_type_json->valuestring;
  pthread_mutex_lock(&rng_mutex[target]);
  distribution_list *list = get_distribution(distribution_type);
  if(!list) {
    perror("Unsupported distribution given.\n"); exit(1);
  }
  void *arguments = list->setup(json);
  if(!arguments) {
    char **buf;
    asprintf(buf, "Invalid JSON for %s distribution.\n", distribution_type);
    perror(*buf); exit(1);
  }
  rng_list *new = malloc(sizeof(rng_list));
  new->next = rng_table[target];
  new->type = distribution_type;
  new->key = key;
  new->args = arguments;
  rng_table[target] = new;
  pthread_mutex_unlock(&rng_mutex[target]);
  return 0;
}

double generate_random_number(char *distribution_type, void *arguments)
{
  hashed target = hash(distribution_type);
  pthread_mutex_lock(&distributions_mutex[target]);
  distribution_list *list = distribution_table[target];
  distribution_list *p = NULL;
  for(p = list; p != NULL; p = p->next)
    {
      if( strcmp(distribution_type, p->key) == 0 )
	{
	  pthread_mutex_unlock(&distributions_mutex[target]);
	  return p->get_number(arguments);
	}
    }
  pthread_mutex_unlock(&distributions_mutex[target]);
  return 0;
}

void add_distribution(char *distribution_type, void*(*setup_from_json)(cJSON *), double(*generate)(void *))
{
  hashed target = hash(distribution_type);
  pthread_mutex_lock(&distributions_mutex[target]);
  distribution_list *new = malloc(sizeof(distribution_list));
  new->next = distribution_table[target];
  new->key = distribution_type;
  new->setup = setup_from_json;
  new->get_number = generate;
  distribution_table[target] = new;
  pthread_mutex_unlock(&distributions_mutex[target]);
}

distribution_list *get_distribution(char *distribution_type) {
  hashed target = hash(distribution_type);
  pthread_mutex_lock(&distributions_mutex[target]);
  distribution_list *list = distribution_table[target];
  distribution_list *p;
  for(p = list; p != NULL; p = p->next)
    {
      if(strcmp(distribution_type, p->key) == 0)
        {
          pthread_mutex_unlock(&distributions_mutex[target]);
          return p;
        }
    }
  pthread_mutex_unlock(&distributions_mutex[target]);
  return NULL;
}

double rand_const(void *arguments) {
  struct constant_distribution *d = (struct constant_distribution*)arguments;
  return d->constant;
}

double rand_exp(void *arguments)
{
  struct exponential_distribution  *d = (struct exponential_distribution*) arguments;
  return gsl_ran_exponential(wg_rng, d->lambda);
}

double rand_norm(void *arguments)
{
  struct normal_distribution *d = (struct normal_distribution*)arguments;
  double x = gsl_ran_gaussian(wg_rng, d->std);
  return d->mean + x;
}

double rand_uni(void *arguments)
{
  struct uniform_distribution *d = (struct uniform_distribution*)arguments;
  return gsl_ran_flat(wg_rng, d->min, d->max);
}


void setup_mutex_array(pthread_mutex_t mutexes[HASHSIZE]) {
  int i = 0;
  for(i; i < HASHSIZE; i++) {
    if( pthread_mutex_init(&mutexes[i],NULL) != 0) { /* mutex must be initialized */
      perror("Mutex could not be initialized.\n");
      exit(1);
    }
  }
}
